namespace SPDeploy.Library
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using System.Xml.Serialization;
    using Data;
    using Exceptions;
    using Helpers;
    using Interfaces;
    using Processing.Library;

    public class DeploymentConfigRunner
    {
        #region Fields: private

        private readonly IResolver<IDeploymentTargetProcessor> _resolver;
        private readonly ITraversalStrategy _traversalStrategy;

        #endregion

        public DeploymentConfigRunner(IResolver<IDeploymentTargetProcessor> resolver, ITraversalStrategy traversalStrategy)
        {
            _resolver = resolver;
            _traversalStrategy = traversalStrategy;
            LogWriter = new MutedLogWriter();
        }

        #region Properties: public

        public ILogWriter LogWriter { get; set; }

        #endregion

        #region Methods: public

        public void Process(ProcessorModes execMode, Deployment deploymentConfig)
        {
            try
            {
                _traversalStrategy.Reset();
                _traversalStrategy.Add(new DeploymentTargetNode {TargetConfigObject = deploymentConfig});
                while (_traversalStrategy.HasItems)
                {
                    var configTargetNode = _traversalStrategy.Next<DeploymentTargetNode>();
                    IEnumerable<PropertyInfo> children = GetChildProperties(configTargetNode, execMode);
                    foreach (PropertyInfo childProperty in children)
                    {
                        if (IsDeploymentConfigProperty(childProperty))
                        {
                            if (childProperty.PropertyType.IsArray)
                            {
                                ProcessArrayOfConfigNodes(configTargetNode, childProperty, execMode);
                            }
                            else if (childProperty.PropertyType.IsClass)
                            {
                                ProcessSingleConfigNode(configTargetNode, childProperty, execMode);
                            }
                        }
                    }
                }
            }
            catch (TargetInvocationException e)
            {
                if (e.InnerException != null)
                {
                    throw e.InnerException;
                }
                throw;
            }
        }

        #endregion

        #region Methods: private

        private static void ExecuteInstall(IDeploymentTargetProcessor processor, DeploymentTargetNode targetNode)
        {
            MethodInfo method = processor.GetType().GetMethod("Install", BindingFlags.Public | BindingFlags.Instance);
            ReuseDeploymentContextFromParentTarget(targetNode);
            object retValue = method.Invoke(processor, new[] {targetNode.ProcessorContext, targetNode.TargetConfigObject});
            targetNode.ProcessorContext.ParentResult = retValue;
        }

        private static void ExecuteTargetProcessor(IDeploymentTargetProcessor processor, DeploymentTargetNode targetNode, ProcessorModes modes)
        {
            switch (modes)
            {
                case ProcessorModes.Install:
                    ExecuteInstall(processor, targetNode);
                    return;

                case ProcessorModes.Uninstall:
                    ExecuteUninstall(processor, targetNode);
                    return;

                case ProcessorModes.Update:
                    ExecuteUpdate(processor, targetNode);
                    return;

                default:
                    throw new DeploymentConfigProcessorException(
                        "Internal error in DeploymentConfigRunner. IDeploymentTargetProcessor interface inconsistencies with current ProcessorModes");
            }
        }

        private static void ExecuteUninstall(IDeploymentTargetProcessor processor, DeploymentTargetNode targetNode)
        {
            MethodInfo method = processor.GetType().GetMethod("Uninstall", BindingFlags.Public | BindingFlags.Instance);
            ReuseDeploymentContextFromParentTarget(targetNode);
            object retValue = method.Invoke(processor, new[] {targetNode.ProcessorContext, targetNode.TargetConfigObject});
            targetNode.ProcessorContext.ParentResult = retValue;
        }

        private static void ExecuteUpdate(IDeploymentTargetProcessor processor, DeploymentTargetNode targetNode)
        {
            MethodInfo method = processor.GetType().GetMethod("Update", BindingFlags.Public | BindingFlags.Instance);
            ReuseDeploymentContextFromParentTarget(targetNode);
            object retValue = method.Invoke(processor, new[] {targetNode.ProcessorContext, null, targetNode.TargetConfigObject});
            targetNode.ProcessorContext.ParentResult = retValue;
        }

        private static IEnumerable<PropertyInfo> GetChildProperties(DeploymentTargetNode configTargetNode, ProcessorModes execMode)
        {
            IEnumerable<PropertyInfo> children =
                configTargetNode.TargetConfigObject.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public)
                    .OrderBy(GetNodeOrderValue, new NodeOrderComparer());
            if (execMode == ProcessorModes.Uninstall)
            {
                children = children.Reverse();
            }
            return children;
        }

        private static int GetNodeOrderValue(ICustomAttributeProvider node)
        {
            object[] attr = node.GetCustomAttributes(typeof (XmlElementAttribute), false);
            if (attr == null || attr.Length == 0)
            {
                return -1;
            }
            return ((XmlElementAttribute) attr[0]).Order;
        }

        private static bool IsDeploymentConfigProperty(PropertyInfo property)
        {
            return property.PropertyType.Namespace.StartsWith("SPDeploy.Data");
        }

        private void ProcessArrayOfConfigNodes(DeploymentTargetNode parentNode, PropertyInfo property, ProcessorModes execMode)
        {
            var array = property.GetValue(parentNode.TargetConfigObject, null) as Array;
            if (array != null)
            {
                foreach (object element in array)
                {
                    ProcessConfigNode(parentNode, element, execMode);
                }
            }
        }

        private void ProcessConfigNode(DeploymentTargetNode parentNode, object configNode, ProcessorModes execMode)
        {
            Type configNodeType = configNode.GetType();
            LogWriter.Write(LogSeverity.Verbose, "DeploymentConfigRunner", "Processing object '{0}'", configNodeType.Name);

            var targetNode = new DeploymentTargetNode
                                 {
                                     TargetConfigObject = configNode,
                                     ParentTargetNode = parentNode,
                                     ProcessorContext = new ProcessorContext()
                                 };

            IDeploymentTargetProcessor processor = _resolver.Resolve(configNodeType);
            if (processor != null)
            {
                LogWriter.Write(LogSeverity.Verbose, "DeploymentConfigRunner", "> Resolved matching processor ({0})", processor.GetType().Name);
                ExecuteTargetProcessor(processor, targetNode, execMode);
            }
            else
            {
                LogWriter.Write(LogSeverity.Verbose, "DeploymentConfigRunner", "> No processor registered");
            }

            if (!targetNode.ProcessorContext.ChildrenDeleted)
            {
                _traversalStrategy.Add(targetNode);
            }
        }

        private void ProcessSingleConfigNode(DeploymentTargetNode parentNode, PropertyInfo property, ProcessorModes execMode)
        {
            object value = property.GetValue(parentNode.TargetConfigObject, null);
            if (value != null)
            {
                ProcessConfigNode(parentNode, value, execMode);
            }
        }

        private static void ReuseDeploymentContextFromParentTarget(DeploymentTargetNode targetNode)
        {
            if (targetNode.ParentTargetNode != null && targetNode.ParentTargetNode.ProcessorContext != null)
            {
                targetNode.ProcessorContext = targetNode.ParentTargetNode.ProcessorContext.Clone();
            }
            else
            {
                targetNode.ProcessorContext = new ProcessorContext();
            }
        }

        #endregion

        #region Nested type: DeploymentTargetNode

        public class DeploymentTargetNode
        {
            #region Properties: public

            public DeploymentTargetNode ParentTargetNode { get; set; }
            public ProcessorContext ProcessorContext { get; set; }
            public object TargetConfigObject { get; set; }

            #endregion
        }

        #endregion

        #region Nested type: MutedLogWriter

        private class MutedLogWriter : ILogWriter
        {
            #region ILogWriter Members

            public void Write(LogSeverity severity, string sourceName, string format, params object[] args)
            {
            }

            #endregion
        }

        #endregion

        #region Nested type: NodeOrderComparer

        private class NodeOrderComparer : IComparer<int>
        {
            #region IComparer<int> Members

            public int Compare(int x, int y)
            {
                if (x == y)
                {
                    return 0;
                }
                if (x == -1 || x > y)
                {
                    return 1;
                }
                // y == -1 || x < y
                return -1;
            }

            #endregion
        }

        #endregion
    }
}